. .\File.ps1
. .\Encoder.ps1

class GiteeProject
{
    [ValidateNotNullOrEmpty()]
    [string]$AccessToken
    [ValidateNotNullOrEmpty()]
    [string]$RepositoryName
    [ValidateNotNullOrEmpty()]
    [string]$Owner
    # 默认使用 master 分支
    [ValidateNotNullOrEmpty()]
    [string]$Reference = "master"

    GiteeProject(
        [string]$AccessToken, 
        [string]$RepositoryName, 
        [string]$Owner
    ){
        $this.Owner = Encode-String -Value $Owner
        $this.AccessToken = $AccessToken
        $this.RepositoryName = Encode-String -Value $RepositoryName
    }

    [object] GetContent([string]$FilePath)
    {
        $encodedPath = Encode-String -Value $FilePath
        $baseUrl = "https://gitee.com/api/v5/repos/$($this.Owner)/$($this.RepositoryName)/contents/$($encodedPath)"
        $parameters = @{
            access_token = $this.AccessToken;
            ref=$this.Reference
        }

        $url = Encode-Url -BaseUrl $baseUrl -Parameters $parameters
        $response = Invoke-WebRequest -Uri $url
        $streamReader = [System.IO.StreamReader]::new($response.RawContentStream,[System.Text.Encoding]::UTF8)
        $allText = $streamReader.ReadToEnd()
        $streamReader.Close()
        $jsonObject = $allText | ConvertFrom-Json

        return $jsonObject;
    }

    [bool] Exists([string]$filePath)
    {
        return $this.GetContent($filePath) -is [array]
    }

    [void] Create([string]$sourceFile,[string]$filePath)
    {
        if (![System.IO.File]::Exists($sourceFile))
        {
            throw "$($sourceFile) 文件不存在！"
        }

        $encodedFilePath = Encode-String -Value $filePath
        $url = "https://gitee.com/api/v5/repos/$($this.Owner)/$($this.RepositoryName)/contents/$encodedFilePath"
        $bytes = [System.IO.File]::ReadAllBytes($sourceFile)
        $content = [Convert]::ToBase64String($bytes)
        $body = @{
            access_token=$this.AccessToken;
            content=$content;
            message="create file $($filePath)";
            branch=$this.Reference
        }
        $jsonBody = $body | ConvertTo-Json

        Invoke-WebRequest -Uri $url -Body $jsonBody -Method Post -ContentType "application/json"
    }

    [void] Update([string]$sourceFile,[object]$fileInfo)
    {
        if (![System.IO.File]::Exists($sourceFile))
        {
            throw "$($sourceFile) 文件不存在！"
        }

        $encodedFilePath = Encode-String -Value $fileInfo.path
        $bytes = [System.IO.File]::ReadAllBytes($sourceFile)
        $content = [Convert]::ToBase64String($bytes)
        $url = "https://gitee.com/api/v5/repos/$($this.Owner)/$($this.RepositoryName)/contents/$($encodedFilePath)"
        $body = @{
            access_token=$this.AccessToken;
            content=$content;
            message="update file $($fileInfo.path)";
            branch=$this.Reference;
            sha=$fileInfo.sha;
        }
        $jsonBody = $body | ConvertTo-Json

        Invoke-WebRequest -Uri $url -Body $jsonBody -Method Put -ContentType "application/json"
    }

    [void] Push([string]$sourceDir, [string[]]$ignorePaths)
    {
        if (![System.IO.Directory]::Exists($sourceDir))
        {
            throw "$($sourceDir) directory not exist!"
        }

        $directoryFullName = [System.IO.Path]::GetFullPath($sourceDir)

        $fileInfos = Get-FileInfos -Directory $directoryFullName -IgnorePaths $ignorePaths
        $projectFileInfos = $this.GetFileInfos()
        $shaList = $this.ReadShaList($sourceDir)

        foreach($fileInfo in $fileInfos)
        {
            $destPath = $fileInfo.FullName.Replace($directoryFullName,"").Trim("\").Replace("\","/")
            [object]$projectFileInfo = $null

            foreach($projectFileInfoItem in $projectFileInfos)
            {
                if ($projectFileInfoItem.path -eq $destPath)
                {
                    $projectFileInfo = $projectFileInfoItem
                    break;
                }
            }

            if ($null -eq $projectFileInfo)
            {
                Write-Host "create file $($destPath) ..."
                $this.Create($fileInfo.FullName, $destPath)
            }
            else
            {
                # 从本地缓存检查内容是否变更
                if (!$this.ContentHasChanged($projectFileInfo, $shaList))
                {
                    Write-Host "$($destPath) no changed."
                    continue
                }

                Write-Host "update file $($destPath) ..."
                $this.Update($fileInfo.FullName, $projectFileInfo)
            }
        }

        # 重新从远程仓库更新本地 sha
        $this.WriteShaList($sourceDir, $this.GetFileInfos())
    }

    hidden [bool]ContentHasChanged([object]$projectFileInfo, [object]$shaList)
    {
        # 如果没有设置本地的 sha ，则直接创建文件
        if ($shaList.Count -eq 0)
        {
            return $true
        }

        $hasChanged = $true

        foreach($shaInfoItem in $shaList)
        {
            if ($shaInfoItem.path -eq $projectFileInfo.path)
            {
                # 如果 sha 相同，则内容未发生改变
                if ($shaInfoItem.sha -eq $projectFileInfo.sha)
                {
                    $hasChanged = $false
                }

                break
            }
        }
       
        return $hasChanged
    }

    [void] WriteShaList([string]$directory, [object]$fileInfos)
    {
        $directoryFullName = [System.IO.Path]::GetFullPath($directory)
        $shaFile = [System.IO.Path]::Combine($directoryFullName,".sha")

        $lines = $fileInfos | ForEach-Object {
            "$($_.path):$($_.sha)"
        }

        Write-Host "update local .sha file."

        [System.IO.File]::WriteAllLines($shaFile, $lines, [System.Text.Encoding]::UTF8)
    }

    [object] ReadShaList([string]$directory)
    {
        $directoryFullName = [System.IO.Path]::GetFullPath($directory)
        $shaFile = [System.IO.Path]::Combine($directoryFullName,".sha")

        # 文件不存在，创建一个空数组
        if (![System.IO.File]::Exists($shaFile))
        {
            Write-Host ".sha not exist."
            return @()
        }

        $lines = [System.IO.File]::ReadAllLines($shaFile)

        $shaList = $lines | ForEach-Object{
            $values = $_.Split(":")
            @{path=$values[0]; sha=$values[1]}
        }

        return $shaList
    }

    [object] GetFileInfos()
    {
        $baseUrl = "https://gitee.com/api/v5/repos/$($this.Owner)/$($this.RepositoryName)/contents"
        $stack = [System.Collections.Generic.Stack[System.String]]::new()
        $stack.Push("/");
        $fileInfos = [System.Collections.Generic.List[System.Object]]::new()

        while([System.Linq.Enumerable]::Any($stack))
        {
            $directory = $stack.Pop()
            $encodedDirectory = Encode-String -Value $directory
            $targetUrl = $baseUrl + "/" + $encodedDirectory
            
            $parameters = @{
                access_token = $this.AccessToken;
                ref = $this.Reference
            }

            $finalUrl = Encode-Url -BaseUrl $targetUrl -Parameters $parameters
            $response = Invoke-WebRequest -Uri $finalUrl -ContentType "application/json" -Method Get
            $streamReader = [System.IO.StreamReader]::new($response.RawContentStream,[System.Text.Encoding]::UTF8)
            $allText = $streamReader.ReadToEnd()
            $streamReader.Close()
            $jsonObject = $allText | ConvertFrom-Json

            foreach($pathInfo in $jsonObject)
            {
                if ($pathInfo.type -eq "file")
                {
                    $fileInfos.Add($pathInfo)
                }
                elseif ($pathInfo.type -eq "dir")
                {
                    $stack.Push($pathInfo.path)
                }
            }
        }

        return $fileInfos
    }

    [void] Delete([object]$fileInfo)
    {
        $encodedPath = Encode-String -Value $fileInfo.path
        $baseUrl = "https://gitee.com/api/v5/repos/$($this.Owner)/$($this.RepositoryName)/contents/$($encodedPath)"
        
        $parameters = @{
            access_token = $this.AccessToken;
            ref = $this.Reference;
            sha = $fileInfo.sha;
            message = "delete $($fileInfo.path) ."
        }
        
        $requestUrl = Encode-Url -BaseUrl $baseUrl -Parameters $parameters

        Invoke-WebRequest -Uri $requestUrl -Method Delete -ContentType "application/json"
    }

    [void] CleanAfterPush([string]$sourceDir, [string[]]$ignorePaths)
    {
        if (![System.IO.Directory]::Exists($sourceDir))
        {
            throw "$($sourceDir) 目录不存在！"
        }

        $directoryFullName = [System.IO.Path]::GetFullPath($sourceDir)
        $fileInfos = Get-FileInfos -Directory $directoryFullName -IgnorePaths $ignorePaths
        $projectFileInfos = $this.GetFileInfos()
        $filePaths = [System.Collections.Generic.List[System.String]]::new()
        $shaList = $this.ReadShaList($directoryFullName)

        foreach($fileInfo in $fileInfos)
        {
            $destPath = $fileInfo.FullName.Replace($directoryFullName,"").Trim("\").Replace("\","/")
            $filePaths.Add($destPath)
            [object]$projectFileInfo = $null

            foreach($projectFileInfoItem in $projectFileInfos)
            {
                if ($projectFileInfoItem.path -eq $destPath)
                {
                    $projectFileInfo = $projectFileInfoItem
                    break;
                }
            }

            if ($null -eq $projectFileInfo)
            {
                Write-Host "create file $($destPath) ..."
                $this.Create($fileInfo.FullName, $destPath)
            }
            else
            {
                # 从本地缓存检查内容是否变更
                if (!$this.ContentHasChanged($projectFileInfo, $shaList))
                {
                    Write-Host "$($destPath) no changed."
                    continue
                }

                Write-Host "update file $($destPath) ..."
                $this.Update($fileInfo.FullName, $projectFileInfo)
            }
        }

        $this.Delete($filePaths, $projectFileInfos)
        # 重新从远程仓库更新本地 sha
        $this.WriteShaList($sourceDir, $this.GetFileInfos())
    }

    # 删除 filePaths 之外的远程仓库中的文件
    hidden [void]Delete([string[]]$filePaths, [object[]]$projectFileInfos)
    {
        [object]$projectFileInfo = $null

        foreach($projectFileInfoItem in $projectFileInfos)
        {
            $hasFile = $false

            foreach($filePath in $filePaths)
            {
                if ($filePath -eq $projectFileInfoItem.path)
                {
                    $hasFile = $true
                }
            }

            # 如果远程目录中存在文件，本地中不存在，那么删除远程仓库中的文件
            if (!$hasFile)
            {
                Write-Host "delete $($projectFileInfoItem.path) ."
                $this.Delete($projectFileInfoItem)
            }
        }
    }

    [void] Push([string]$sourceDir, [string[]]$ignorePaths,[bool]$isClean)
    {
        if (!$isClean)
        {
            $this.Push($sourceDir, $ignorePaths)
        }
        else
        {
            $this.CleanAfterPush($sourceDir, $ignorePaths)
        }
    }

    [void] Pull([string]$destDirectory, [string]$isClean)
    {
        $fullDestDirectory = [System.IO.Path]::GetFullPath($destDirectory)

        if (![System.IO.Directory]::Exists($destDirectory))
        {
            [System.IO.Directory]::CreateDirectory($fullDestDirectory)
        }

        $fileInfos = $this.GetFileInfos()
        $filePaths = [System.Collections.Generic.List[System.String]]::new()
        $shaList = $this.ReadShaList($fullDestDirectory)
      
        ForEach-Object{Write-Host "$($_.path) $($_.sha)"} 

        foreach($fileInfo in $fileInfos)
        {
            # 如果内容没有改变，则不更新本地文件
            if (!$this.ContentHasChanged($fileInfo, $shaList))
            {
                Write-Host "$($fileInfo.path) no changed."
                continue
            }

            $filePath = [System.IO.Path]::Combine($fullDestDirectory, $fileInfo.path)
            $fullDirectoryName = [System.IO.Path]::GetDirectoryName($filePath)

            # 目录不存在，则创建目录
            if (![System.IO.Directory]::Exists($fullDirectoryName))
            {
                [System.IO.Directory]::CreateDirectory($fullDirectoryName)
            }

            $filePaths.Add($filePath)
            $fileContentInfo = $this.GetContent($fileInfo.path)
            $bytes = [Convert]::FromBase64String($fileContentInfo.content)
            $allText = [System.Text.Encoding]::UTF8.GetString($bytes)

            Write-Host "pull $($fileInfo.path) ..."
            [System.IO.File]::WriteAllText($filePath, $allText, [System.Text.Encoding]::UTF8)
        }

        if ($isClean)
        {
            $this.DeleteLocalFile($destDirectory, $fileInfos)
        }

        $this.WriteShaList($fullDestDirectory, $fileInfos)
    }

    hidden [void] DeleteLocalFile([string]$directory, [object[]]$projectFileInfos)
    {
        $fullDirectoryPath = [System.IO.Path]::GetFullPath($directory)
        $fileInfos = Get-FileInfos -Directory $fullDirectoryPath -IgnorePaths @()

        foreach($fileInfo in $fileInfos)
        {
            $hasFile = $false

            foreach($projectFileInfo in $projectFileInfos)
            {
                $fullPath = [System.IO.Path]::Combine($fullDirectoryPath, $projectFileInfo.path).Replace("/","\")

                if ($fullPath -eq $fileInfo.FullName)
                {
                    $hasFile = $true
                    break;
                }
            }

            # 如果远程仓库中不存在该文件，则删除本地文件
            if (!$hasFile){
                Write-Host "delete $($fileInfo.FullName)"
                [System.IO.File]::Delete($fileInfo.FullName)
            }
        }
    }
}